package org.loong.crypto.service.software.factory;

import java.security.Key;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Objects;

import javax.crypto.SecretKey;

import org.apache.commons.lang3.ArrayUtils;
import org.loong.crypto.common.constant.CryptoConstants;
import org.loong.crypto.common.exception.CryptoException;
import org.loong.crypto.common.exception.InvalidKeyTypeException;
import org.loong.crypto.core.algorithm.CipherAlgorithm;
import org.loong.crypto.core.algorithm.HmacAlgorithm;
import org.loong.crypto.core.algorithm.KeyAlgorithm;
import org.loong.crypto.core.algorithm.KeyPairAlgorithm;
import org.loong.crypto.core.algorithm.MessageDigestAlgorithm;
import org.loong.crypto.core.algorithm.SignatureAlgorithm;
import org.loong.crypto.core.params.CipherParameters;
import org.loong.crypto.core.params.HmacParameters;
import org.loong.crypto.core.params.MessageDigestParameters;
import org.loong.crypto.core.params.SignatureParameters;
import org.loong.crypto.service.core.provider.Decrypter;
import org.loong.crypto.service.core.provider.Encrypter;
import org.loong.crypto.service.core.provider.HmacSigner;
import org.loong.crypto.service.core.provider.HmacVerifier;
import org.loong.crypto.service.core.provider.KeyGenerator;
import org.loong.crypto.service.core.provider.KeyPairGenerator;
import org.loong.crypto.service.core.provider.MessageDigest;
import org.loong.crypto.service.core.provider.ProviderFactory;
import org.loong.crypto.service.core.provider.RandomGenerator;
import org.loong.crypto.service.core.provider.Signer;
import org.loong.crypto.service.core.provider.Verifier;
import org.loong.crypto.service.software.provider.asymmetric.dsa.DSASigner;
import org.loong.crypto.service.software.provider.asymmetric.dsa.DSAVerifier;
import org.loong.crypto.service.software.provider.asymmetric.ec.ECIESDecrypter;
import org.loong.crypto.service.software.provider.asymmetric.ec.ECIESEncrypter;
import org.loong.crypto.service.software.provider.asymmetric.ecdsa.ECDSASigner;
import org.loong.crypto.service.software.provider.asymmetric.ecdsa.ECDSAVerifier;
import org.loong.crypto.service.software.provider.asymmetric.rsa.RSADecrypter;
import org.loong.crypto.service.software.provider.asymmetric.rsa.RSAEncrypter;
import org.loong.crypto.service.software.provider.asymmetric.rsa.RSASigner;
import org.loong.crypto.service.software.provider.asymmetric.rsa.RSAVerifier;
import org.loong.crypto.service.software.provider.asymmetric.sm2.SM2Decrypter;
import org.loong.crypto.service.software.provider.asymmetric.sm2.SM2Encrypter;
import org.loong.crypto.service.software.provider.asymmetric.sm2.SM2Signer;
import org.loong.crypto.service.software.provider.asymmetric.sm2.SM2Verifier;
import org.loong.crypto.service.software.provider.digest.GenericMessageDigest;
import org.loong.crypto.service.software.provider.hmac.GenericHmacSinger;
import org.loong.crypto.service.software.provider.hmac.GenericHmacVerifier;
import org.loong.crypto.service.software.provider.key.GenericKeyGenerator;
import org.loong.crypto.service.software.provider.key.GenericKeyPairGenerator;
import org.loong.crypto.service.software.provider.random.GenericRandomGenerator;
import org.loong.crypto.service.software.provider.symmetric.aes.AESDecrypter;
import org.loong.crypto.service.software.provider.symmetric.aes.AESEncrypter;
import org.loong.crypto.service.software.provider.symmetric.des.DESedeDecrypter;
import org.loong.crypto.service.software.provider.symmetric.des.DESedeEncrypter;
import org.loong.crypto.service.software.provider.symmetric.sm4.SM4Decrypter;
import org.loong.crypto.service.software.provider.symmetric.sm4.SM4Encrypter;

public class SoftwareProviderFactory implements ProviderFactory {

    @Override
    public RandomGenerator createRandomGenerator() throws CryptoException {
        return new GenericRandomGenerator();
    }

    @Override
    public KeyGenerator createKeyGenerator(KeyAlgorithm algorithm) throws CryptoException {
        KeyGenerator keyGenerator;
        if (GenericKeyGenerator.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            keyGenerator = new GenericKeyGenerator();
        } else {
            throw new CryptoException("Unsupported key algorithm: " + algorithm);
        }

        return keyGenerator;
    }

    @Override
    public KeyPairGenerator createKeyPairGenerator(KeyPairAlgorithm algorithm) throws CryptoException {
        KeyPairGenerator keyPairGenerator;
        if (GenericKeyPairGenerator.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            keyPairGenerator = new GenericKeyPairGenerator();
        } else {
            throw new CryptoException("Unsupported key pair algorithm: " + algorithm);
        }

        return keyPairGenerator;
    }

    @Override
    public MessageDigest createMessageDigest(MessageDigestAlgorithm algorithm, final MessageDigestParameters params) throws CryptoException {
        MessageDigest messageDigest;
        if (GenericMessageDigest.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            messageDigest = new GenericMessageDigest();
        } else {
            throw new CryptoException("Unsupported message digest algorithm: " + algorithm);
        }

        return messageDigest;
    }

    @Override
    public HmacSigner createHmacSigner(final HmacAlgorithm algorithm, final HmacParameters params) throws CryptoException {
        if (Objects.isNull(params) || (Objects.isNull(params.getKeyIndex()) && ArrayUtils.isEmpty(params.getSecret()))) {
            throw new CryptoException("Expected secret key but none available.");
        }

        if (Objects.nonNull(params.getKeyIndex()) && ArrayUtils.isEmpty(params.getSecret())) {
            params.setSecret(CryptoConstants.DEFAULT_KEY);
        }
        
        HmacSigner hmacSigner;
        if (GenericHmacSinger.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            hmacSigner = new GenericHmacSinger(params.getSecret());
        } else {
            throw new CryptoException("Unsupported hmac algorithm: " + algorithm);
        }

        return hmacSigner;
    }

    @Override
    public HmacVerifier createHmacVerifier(final HmacAlgorithm algorithm, final HmacParameters params) throws CryptoException {
        if (Objects.isNull(params) || (Objects.isNull(params.getKeyIndex()) && ArrayUtils.isEmpty(params.getSecret()))) {
            throw new CryptoException("Expected secret key but none available.");
        }

        if (Objects.nonNull(params.getKeyIndex()) && ArrayUtils.isEmpty(params.getSecret())) {
            params.setSecret(CryptoConstants.DEFAULT_KEY);
        }

        HmacVerifier hmacVerifier;
        if (GenericHmacVerifier.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            hmacVerifier = new GenericHmacVerifier(params.getSecret());
        } else {
            throw new CryptoException("Unsupported hmac algorithm: " + algorithm);
        }

        return hmacVerifier;
    }

    @Override
    public Encrypter createEncrypter(CipherAlgorithm algorithm, CipherParameters params) throws CryptoException {
        if (Objects.isNull(params)) {
            throw new CryptoException("Expected cipher parameters but none available.");
        }

        if (CipherAlgorithm.Family.SYMMETRIC.contains(algorithm) && Objects.nonNull(params.getKeyIndex())) {
            params.setKeyBytes(CryptoConstants.DEFAULT_KEY);
        } else if (Objects.isNull(params.getKey()) && ArrayUtils.isEmpty(params.getKeyBytes())) {
            throw new CryptoException("Expected key but none available.");
        }

        final Key key = params.getKey();
        final byte[] keyBytes = params.getKeyBytes();

        Encrypter encrypter;
        if (AESEncrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !AESEncrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported encrypt algorithm: " + params.getEncryptionMethod().getAlgorithm());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey aesKey = (SecretKey) key;
                encrypter = new AESEncrypter(aesKey);
            } else {
                encrypter = new AESEncrypter(keyBytes);
            }
        } else if (DESedeEncrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !DESedeEncrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported encrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey desKey = (SecretKey) key;
                encrypter = new DESedeEncrypter(desKey);
            } else {
                encrypter = new DESedeEncrypter(keyBytes);
            }
        } else if (SM4Encrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !SM4Encrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported encrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey sm4Key = (SecretKey) key;
                encrypter = new SM4Encrypter(sm4Key);
            } else {
                encrypter = new SM4Encrypter(keyBytes);
            }
        } else if (ECIESEncrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !ECIESEncrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported encrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPublicKey)) {
                    throw new InvalidKeyTypeException(ECPublicKey.class);
                }

                ECPublicKey ecPublicKey = (ECPublicKey) key;
                encrypter = new ECIESEncrypter(ecPublicKey);
            } else {
                encrypter = new ECIESEncrypter(keyBytes);
            }
        } else if (RSAEncrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !RSAEncrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported encrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof RSAPublicKey)) {
                    throw new InvalidKeyTypeException(RSAPublicKey.class);
                }

                RSAPublicKey rsaPublicKey = (RSAPublicKey) key;
                encrypter = new RSAEncrypter(rsaPublicKey);
            } else {
                encrypter = new RSAEncrypter(keyBytes);
            }
        } else if (SM2Encrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPublicKey)) {
                    throw new InvalidKeyTypeException(ECPublicKey.class);
                }

                ECPublicKey ecPublicKey = (ECPublicKey) key;
                encrypter = new SM2Encrypter(ecPublicKey);
            } else {
                encrypter = new SM2Encrypter(keyBytes);
            }
        } else {
            throw new CryptoException("Unsupported encrypt algorithm: " + algorithm);
        }

        encrypter.init(params);
        return encrypter;
    }

    @Override
    public Decrypter createDecrypter(CipherAlgorithm algorithm, CipherParameters params) throws CryptoException {
        if (Objects.isNull(params)) {
            throw new CryptoException("Expected cipher parameters but none available.");
        }

        if (CipherAlgorithm.Family.SYMMETRIC.contains(algorithm) && Objects.nonNull(params.getKeyIndex())) {
            params.setKeyBytes(CryptoConstants.DEFAULT_KEY);
        } else if (Objects.isNull(params.getKey()) && ArrayUtils.isEmpty(params.getKeyBytes())) {
            throw new CryptoException("Expected key but none available.");
        }

        final Key key = params.getKey();
        final byte[] keyBytes = params.getKeyBytes();

        Decrypter decrypter;
        if (AESDecrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !AESDecrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported decrypt algorithm: " + params.getEncryptionMethod().getAlgorithm());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey aesKey = (SecretKey) key;
                decrypter = new AESDecrypter(aesKey);
            } else {
                decrypter = new AESDecrypter(keyBytes);
            }
        } else if (DESedeDecrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !DESedeDecrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported decrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey desKey = (SecretKey) key;
                decrypter = new DESedeDecrypter(desKey);
            } else {
                decrypter = new DESedeDecrypter(keyBytes);
            }
        } else if (SM4Decrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !SM4Decrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported decrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof SecretKey)) {
                    throw new InvalidKeyTypeException(SecretKey.class);
                }

                SecretKey sm4Key = (SecretKey) key;
                decrypter = new SM4Decrypter(sm4Key);
            } else {
                decrypter = new SM4Decrypter(keyBytes);
            }
        } else if (ECIESDecrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !ECIESDecrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported decrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPrivateKey)) {
                    throw new InvalidKeyTypeException(ECPrivateKey.class);
                }

                ECPrivateKey ecPrivateKey = (ECPrivateKey) key;
                decrypter = new ECIESDecrypter(ecPrivateKey);
            } else {
                decrypter = new ECIESDecrypter(keyBytes);
            }
        } else if (RSADecrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(params.getEncryptionMethod()) && !RSADecrypter.SUPPORTED_ENCRYPTION_METHODS.contains(params.getEncryptionMethod())) {
                throw new CryptoException("Unsupported decrypt algorithm: " + params.getEncryptionMethod());
            }

            if (Objects.nonNull(key)) {
                if (!(key instanceof RSAPrivateKey)) {
                    throw new InvalidKeyTypeException(RSAPrivateKey.class);
                }

                RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) key;
                decrypter = new RSADecrypter(rsaPrivateKey);
            } else {
                decrypter = new RSADecrypter(keyBytes);
            }
        } else if (SM2Decrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPrivateKey)) {
                    throw new InvalidKeyTypeException(ECPrivateKey.class);
                }

                ECPrivateKey ecPrivateKey = (ECPrivateKey) key;
                decrypter = new SM2Decrypter(ecPrivateKey);
            } else {
                decrypter = new SM2Decrypter(keyBytes);
            }
        } else {
            throw new CryptoException("Unsupported decrypt algorithm: " + algorithm);
        }

        decrypter.init(params);
        return decrypter;
    }

    @Override
    public Signer createSigner(final SignatureAlgorithm algorithm, final SignatureParameters params) throws CryptoException {
        if (Objects.isNull(params) || (Objects.isNull(params.getKey()) && ArrayUtils.isEmpty(params.getKeyBytes()))) {
            throw new CryptoException("Expected private key but none available.");
        }

        final Key key = params.getKey();
        final byte[] keyBytes = params.getKeyBytes();

        Signer signer;
        if (RSASigner.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof RSAPrivateKey)) {
                    throw new InvalidKeyTypeException(RSAPrivateKey.class);
                }

                RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) key;
                signer = new RSASigner(rsaPrivateKey);
            } else {
                signer = new RSASigner(keyBytes);
            }
        } else if (SM2Signer.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPrivateKey)) {
                    throw new InvalidKeyTypeException(ECPrivateKey.class);
                }

                ECPrivateKey ecPrivateKey = (ECPrivateKey) key;
                signer = new SM2Signer(ecPrivateKey);
                signer.init(params);
            } else {
                signer = new SM2Signer(keyBytes);
                signer.init(params);
            }
        } else if (DSASigner.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof DSAPrivateKey)) {
                    throw new InvalidKeyTypeException(DSAPrivateKey.class);
                }

                DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) key;
                signer = new DSASigner(dsaPrivateKey);
            } else {
                signer = new DSASigner(keyBytes);
            }
        } else if (ECDSASigner.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPrivateKey)) {
                    throw new InvalidKeyTypeException(ECPrivateKey.class);
                }

                ECPrivateKey ecPrivateKey = (ECPrivateKey) key;
                signer = new ECDSASigner(ecPrivateKey);
            } else {
                signer = new ECDSASigner(keyBytes);
            }
        } else {
            throw new CryptoException("Unsupported signature algorithm: " + algorithm);
        }

        return signer;
    }

    @Override
    public Verifier createVerifier(final SignatureAlgorithm algorithm, final SignatureParameters params) throws CryptoException {
        if (Objects.isNull(params) || (Objects.isNull(params.getKey()) && ArrayUtils.isEmpty(params.getKeyBytes()))) {
            throw new CryptoException("Expected public key but none available.");
        }

        final Key key = params.getKey();
        final byte[] keyBytes = params.getKeyBytes();

        Verifier verifier;
        if (RSAVerifier.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof RSAPublicKey)) {
                    throw new InvalidKeyTypeException(RSAPublicKey.class);
                }

                RSAPublicKey rsaPublicKey = (RSAPublicKey) key;
                verifier = new RSAVerifier(rsaPublicKey);
            } else {
                verifier = new RSAVerifier(keyBytes);
            }
        } else if (SM2Verifier.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPublicKey)) {
                    throw new InvalidKeyTypeException(ECPublicKey.class);
                }

                ECPublicKey ecPublicKey = (ECPublicKey) key;
                verifier = new SM2Verifier(ecPublicKey);
                verifier.init(params);
            } else {
                verifier = new SM2Verifier(keyBytes);
                verifier.init(params);
            }
        } else if (DSAVerifier.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof DSAPublicKey)) {
                    throw new InvalidKeyTypeException(DSAPublicKey.class);
                }

                DSAPublicKey dsaPublicKey = (DSAPublicKey) key;
                verifier = new DSAVerifier(dsaPublicKey);
            } else {
                verifier = new DSAVerifier(keyBytes);
            }
        } else if (ECDSAVerifier.SUPPORTED_ALGORITHMS.contains(algorithm)) {
            if (Objects.nonNull(key)) {
                if (!(key instanceof ECPublicKey)) {
                    throw new InvalidKeyTypeException(ECPublicKey.class);
                }

                ECPublicKey ecPublicKey = (ECPublicKey) key;
                verifier = new ECDSAVerifier(ecPublicKey);
            } else {
                verifier = new ECDSAVerifier(keyBytes);
            }
        } else {
            throw new CryptoException("Unsupported signature algorithm: " + algorithm);
        }

        return verifier;
    }
}
